!pr2
A Disassembler for the 65816...............Bob Sander-Cederlof

When I first got my Apple, there were no books around for learning 6502 assembly language.  It took me about 3 months to locate and buy a copy of the 6502 programmer's manual from MOS Technology.  About the same time I found a book by William Barden that briefly covered the 8080, 6800, and 6502.  But the way I really learned the 6502 was by using Woz's L command in the Apple monitor.

Of course there were no printers or printer interfaces around in those days either, so I spent hours upon hours copying 20 lines at a time off the screen.  I wrote down a lot of the monitor, and all of the floating point package and Sweet-16 from the tail end of the Integer BASIC ROM.  Fortunately, Apple has never gotten around to eliminating the fabulous L-command from the monitor.

In fact, they have even augmented it.  The //c version includes patches to allow disassembly of the additional opcodes and address modes of the 65C02.  Since Rak-Ware's DISASM calls on the ROM disassembler to decipher each line of code, the //c version automatically grows to accomodate the 65C02.

Now, what about the 65802 and 65816?  It's about time someone wrote a disassembler for that.  Someone?  Why not me?

It's not easy.  On the one hand there is the pressure of competition.  Woz's code is SO compact!  On the other hand, the new chip is SO complex!  It is even ambiguous.  There is absolutely no way for a 65816 disassembler to know whether an immediate-mode instruction is two or three bytes long.  Only by executing the programming, and tracing it line-by-line, can we tell.  And even then, it is possible that a tricky programmer might set up code so that it can be interpreted both ways, depending on other conditions.

To make a long story a little shorter, I did it.  You guessed that of course.  My solution to the ambiguity problem was to put the burden on the person using it.  My solution to the complexity problem was to use extensive tables.  My solution to the competition with Woz was to do my best and let him keep his well-deserved glory.

In fact, I started by carefully analyzing Woz's code.  The trail starts at $FE9E in the monitor ROM.  That short piece of code calls INSTDSP at $F8D0 twenty times to disassemble 20 lines of code.  If you take a peek ahead to my listing, lines 1390-1400 patch the language card copy of the monitor inside the L-command loop, so that instead of calling $F8D0 twenty times it calls my disassembler at $0B67 twenty times.  (If you are using the language card version of the S-C Macro Assembler, there is a copy of the monitor in the language card too.)

BRUNning the 65816 disassembler will install this little patch and toggle the immediate-mode size flag.  Thereafter each 800G command will toggle the state of the immediate-mode size flag.  In one state this flag causes immediate mode instructions to be disassembled as 2-byte instructions; in the other, 3-byte instructions.

The tables are quite complicated, and difficult to type in accurately.  Therefore I used macros and let the S-C Macro Assembler do the dirty work.  The first table starts at line 1500, and consists of the packed names of the single byte opcodes.  The macro at lines 1210-1290 defines how the packing is done.  The calling line is of the form ">ON A,B,C,D" where the A, B, and C parameters are the three letters of the opcode name.  The D parameter is the letter "A" on those opcodes which might also be multiple-byte:  ASL, DEC, INC, LSR, ROR, and ROL.

The packing algorithm is almost the same as the one Woz used in the monitor.  Each character is represented by five bits, so that three letters take only 15 bits.  The macro sets L1, L2, and L3 to the ASCII value (less 64) of the letters of the opcode name.  The .SE directive is used for this so that each invocation of the macro can redefine these variables.  This compresses the letters from the range $41...5A to $01...1A.  Then the .DA line uses multiplication and addition to pack up the compressed letters.  Since arithmetic expressions are parsed by the S-C Macro Assembler in a strict left-to-right fashion, "L1*32+L2*32+L3*2" packs them together.

The "ON" macro also generates a label for the opcode name value by using the opcode name, together with the 4th parameter when present.  These names are referred to by another table later on.

The second table is just like the first, but with the names of the longer opcodes instead.  Notice that ASL, DEC, etc are in this table too, but without the 4th parameter.

The third and fourth tables have 256 entries, one for every possible opcode byte.  Each entry is only one byte long, so each table is 256 bytes.  Woz used several smaller tables, because the 6502 didn't use every possible opcode value.  The 65816 does define an opcode name for every possible value.

The OPINDEX table uses two macros: "OXA" for single byte opcodes, and "OXB" for longer opcodes.  Each entry is a pointer to the name in the OPNAMES.A or OPNAMES.B tables.  The pointer is divided by two, leaving room for a flag bit which tells which of the two tables the name is in.

The entries in the OPFORMAT table are offsets into the FMTBL.  These are all multiples of 2, because the FMTBL entries are two bytes each.

FMTBL contains coded information indicating how many bytes comprise the instruction and operand, and what the address mode looks like in assembly language.  The length can be from two to four bytes, and is coded as 1...3 in the last two bits.  The rest of the bits tell which special characters to print and where to print the value of the operand bytes.  Single byte opcodes don't have any entries in this table.

One more table, the last one:  FMTSTR.  This defines the meaning of the bits in FMTBL.  Note that the characters are the same as the ones in the various comment lines within FMTBL, only in reverse order.

Finally, we get to the code.  The 20-line disassembler calls INSTDSP at line 6180.  This starts by calling INSDS1 at line 5760.  INSDS1 and INSDS2 are kept as defined points because other software sometimes calls these two points.  If you wanted to modify Rak-Ware's DISASM, for example, you would probably need these.

Lines 5760-5840 print the address of the next opcode, and "- ".  Lines 5850-5860 pick up that opcode byte.  If you enter at INSDS2, have the opcode byte already in the A-register.  Lines 5870-5980 dig into the tables to get the opcode name, format, and length for single-byte opcodes.  Lines 6000-6160 do the same for longer opcodes.  The differences for longer opcodes are several:  the second opname table is used, the format is gotten from the tables, and the immediate-mode size flag is used to determine the length of immediate mode opcodes.

Lines 6200-6300 print out the 1-4 bytes of the opcode in hex.  If there are less than four bytes, enough blanks are printed so that we always end up in the same position.  Lines 6310-6400 unpack the opcode name and print it out.  If the opcode is single byte, lines 6410-6420 find out and send us back home (we are finished with this line).

Lines 6430-6450 test the format to detect MVP, MVN, and relative address mode instructions.  These special cases are handled by lines 6690-7050.  All other operand formats are handled by lines 6470-6680.  I see now that I could have put lines 6470-6480 back before line 6430, so that the blank separating the opname from the operand was printed before splitting on the mode.  Then lines 6700-6710 could be deleted, saving five bytes.  Of course line 6720 would then receive the ".9" label.

Lines 6500-6520 shift out one bit at a time of the format bit string.  The corresponding index counts down in the X-register from 10 to 0, and picks a format character from FMTSTR to print.  After the character is printed, two special cases are looked for.  If the character was "#", meaning immediate mode, and if the immediate-mode size flag indicates long immediates, another "#" is printed.  If the character was "$", it is time to print the operand in hex, as two, four, or six digits (lines 6620-6650).

Relative addresses may be either 8-bit or 16-bit.  Lines 6780-6820 start the computation for 8-bit values, and call on a monitor routine to finish the printing.  Lines 6840-6950 do the same for 16-bit relatives.  (There are no two-bit relatives here, no matter what the family tree has borne.)

Finally, lines 6970-7050 print out the two bank bytes for the MVP and MVN instructions.  This is different from the way you write MVP and MVN for assembly by the S-C Macro Assembler.  In the assembler you write "MVP addr1,addr2", where both addresses are 24-bit values.  The bank bytes come from the high byte of each 24-bit address.  To be compatible with the assembler I should change lines 6970-7050 to print out "0000" after each bank byte.

It seems like a worthy project for someone to incorporate my program into Rak-Ware's DISASM, or perhaps a new similar product.  If so, that someone should figure out a neat interactive way to control the immediate-mode size flag.  How about it, Bob?
!np
That's enough of that.  The assembly listing of that table expands to about 4 pages, so here's a hex dump of OPNAMES.A and OPNAMES.B (By the way, OPNAMES.B .EQ $881):





















And the OPINDEX table runs about 7 pages, so another hex dump:
















The assembly listing of OPFORMAT is around a page and a half, so we'll just LIST this one:
!np
For a complete source listing of this program send a legal-size self-addressed envelope with 2 ounces postage.  Or, order Quarterly Disk #18 for $15 to get S.65816.DISASM along with all the rest of the source code from the last three issues on disk.
